using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using System.Threading;

namespace Synapse.Framework.AoP
{
    public class AspectDecorator : IAspectDecorator
    {
        private readonly IInvocationHandlerFactory invocationHandlerFactory;
        private AssemblyBuilder assemblyBuilder;

        private readonly Dictionary<Type, OpCode> opCodeTypeMapper;

        public AspectDecorator(IInvocationHandlerFactory invocationHandlerFactory)
        {
            this.invocationHandlerFactory = invocationHandlerFactory;
            opCodeTypeMapper = new Dictionary<Type, OpCode>
                                    {
                                        {typeof (Boolean), OpCodes.Ldind_I1},
                                        {typeof (Int16), OpCodes.Ldind_I2},
                                        {typeof (Int32), OpCodes.Ldind_I4},
                                        {typeof (Int64), OpCodes.Ldind_I8},
                                        {typeof (Double), OpCodes.Ldind_R8},
                                        {typeof (Single), OpCodes.Ldind_R4},
                                        {typeof (UInt16), OpCodes.Ldind_U2},
                                        {typeof (UInt32), OpCodes.Ldind_U4}
                                    };
        }

        public object Create(IResolver resolver, object instance, Type @interface)
        {
            return Activator.CreateInstance(CreateType(@interface), new[] { invocationHandlerFactory.Create(resolver, instance, @interface) });
        }

        private Type CreateType(Type type)
        {
            var handlerType = typeof(IInvocationHandler);
            var typeBuilder = CreateTypeBuilder(type);
            var handlerField = typeBuilder.DefineField(handlerType.Name, handlerType, FieldAttributes.Private);
            var constructor = typeBuilder.DefineConstructor(MethodAttributes.Public,
                                                                                    CallingConventions.Standard,
                                                                                    new[] { handlerType });
            CreateConstructor(constructor, handlerField);
            foreach (var methodInfo in type.GetMethods())
                GenerateMethod(methodInfo, handlerField, typeBuilder);
            return typeBuilder.CreateType();
        }

        private TypeBuilder CreateTypeBuilder(Type type)
        {
            AppDomain domain = Thread.GetDomain();
            AssemblyName assemblyName = CreateAssemblyName(type);
            assemblyBuilder = domain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
            string fullName = type.FullName + "#";
            ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(fullName);
            TypeAttributes typeAttributes = DefineTypeAttributes();
            return moduleBuilder.DefineType(fullName, typeAttributes, typeof(Object), new []{type});
        }

        private  void CreateConstructor(ConstructorBuilder delegateConstructor, FieldInfo handlerField)
        {
            ILGenerator constructorIL = delegateConstructor.GetILGenerator();
            constructorIL.Emit(OpCodes.Ldarg_0);
            constructorIL.Emit(OpCodes.Ldarg_1);
            constructorIL.Emit(OpCodes.Stfld, handlerField);
            constructorIL.Emit(OpCodes.Nop);
            constructorIL.Emit(OpCodes.Ret);
        }

        private static TypeAttributes DefineTypeAttributes()
        {
            return TypeAttributes.Class | TypeAttributes.Public;
        }

        private static AssemblyName CreateAssemblyName(Type type)
        {
            var assemblyName = new AssemblyName
            {
                Name = type.Assembly.GetName().Name,
                Version = new Version(1, 0, 0, 0)
            };
            return assemblyName;
        }

        private void GenerateMethod(MethodInfo methodInfo, FieldInfo handlerField, TypeBuilder typeBuilder)
        {
            ParameterInfo[] methodParams = methodInfo.GetParameters();
            int numberOfParameters = methodParams.Length;
            Type[] methodParameters = DefineMethodParameters(methodParams, numberOfParameters);
            MethodBuilder methodBuilder = CreateMethodBuilder(methodInfo, methodParameters, typeBuilder);
            ILGenerator methodIL = methodBuilder.GetILGenerator();
            int locIndex = DefineShiftLoc(methodInfo);
            CreateHeader(methodInfo, methodParameters, methodIL, handlerField);
            CreateBody(methodParameters, methodIL, methodInfo.Name, locIndex);
            CreateFooter(methodInfo, methodIL);
        }

        private static void CreateBody(Type[] methodParameters, ILGenerator methodIL, string methodName, int locIndex)
        {
            DefineParameters(methodParameters, methodIL, methodName);
            CreateParameters(methodParameters, methodIL, locIndex);
            methodIL.Emit(OpCodes.Call, typeof(InvocationHandler).GetMethod("Invoke"));
        }

        private static void CreateHeader(MethodInfo methodInfo, ICollection<Type> methodParameters, ILGenerator methodIL, FieldInfo handlerField)
        {
            DeclareReturnType(methodInfo, methodIL);
            DeclareParameters(methodParameters, methodIL);
            methodIL.Emit(OpCodes.Nop);
            CallProxy(methodIL, handlerField);
        }

        private void CreateFooter(MethodInfo methodInfo, ILGenerator methodIL)
        {
            if (HasReturnType(methodInfo))
                CreateReturnValue(methodInfo, methodIL);
            else
                methodIL.Emit(OpCodes.Pop);
            methodIL.Emit(OpCodes.Ret);
        }

        private static int DefineShiftLoc(MethodInfo methodInfo)
        {
            return HasReturnType(methodInfo) ? 1 : 0;
        }

        private static void DeclareReturnType(MethodInfo methodInfo, ILGenerator methodIL)
        {
            if (HasReturnType(methodInfo))
                methodIL.DeclareLocal(methodInfo.ReturnType);
        }

        private static bool HasReturnType(MethodInfo methodInfo)
        {
            return !(methodInfo.ReturnType == typeof(void));
        }

        private static void DeclareParameters(ICollection<Type> methodParameters, ILGenerator methodIL)
        {
            if (HasParameters(methodParameters))
            {
                methodIL.DeclareLocal(typeof(object[]));
            }
        }

        private static bool HasParameters(ICollection<Type> methodParameters)
        {
            return methodParameters.Count > 0;
        }

        //private static void DefineParameters(ICollection<Type> methodParameters, ILGenerator methodIL, int methodInfoIndex)
        //{
        //    methodIL.Emit(OpCodes.Ldc_I4, methodInfoIndex);
        //    methodIL.Emit(OpCodes.Ldc_I4, methodParameters.Count);
        //    methodIL.Emit(OpCodes.Newarr, typeof(Object));
        //}

        private static void DefineParameters(ICollection<Type> methodParameters, ILGenerator methodIL, string methodName)
        {
            methodIL.Emit(OpCodes.Ldstr, methodName);
            methodIL.Emit(OpCodes.Ldc_I4, methodParameters.Count);
            methodIL.Emit(OpCodes.Newarr, typeof(Object));
        }

        private static void CallProxy(ILGenerator methodIL, FieldInfo handlerField)
        {
            methodIL.Emit(OpCodes.Ldarg_0);
            methodIL.Emit(OpCodes.Ldfld, handlerField);
        }

        private static void CreateParameters(Type[] methodParameters, ILGenerator methodIL, int locIndex)
        {
            if (HasParameters(methodParameters))
            {
                methodIL.Emit(OpCodes.Stloc, locIndex);
                for (int parameterIndex = 0; parameterIndex < methodParameters.Length; parameterIndex++)
                {
                    methodIL.Emit(OpCodes.Ldloc, locIndex);
                    methodIL.Emit(OpCodes.Ldc_I4, parameterIndex);
                    methodIL.Emit(OpCodes.Ldarg, parameterIndex + 1);
                    if (methodParameters[parameterIndex].IsValueType)
                    {
                        methodIL.Emit(OpCodes.Box, methodParameters[parameterIndex]);
                    }
                    methodIL.Emit(OpCodes.Stelem_Ref);
                }
                methodIL.Emit(OpCodes.Ldloc, locIndex);
            }
        }

        private void CreateReturnValue(MethodInfo methodInfo, ILGenerator methodIL)
        {
            if (methodInfo.ReturnType.IsValueType)
            {
                methodIL.Emit(OpCodes.Unbox, methodInfo.ReturnType);
                if (methodInfo.ReturnType.IsEnum)
                    methodIL.Emit(OpCodes.Ldind_I4);
                else if (!methodInfo.ReturnType.IsPrimitive)
                    methodIL.Emit(OpCodes.Ldobj, methodInfo.ReturnType);
                else
                    methodIL.Emit(opCodeTypeMapper[methodInfo.ReturnType]);
            }
            methodIL.Emit(OpCodes.Stloc_0);
            methodIL.Emit(OpCodes.Ldloc_0);
        }

        private static Type[] DefineMethodParameters(ParameterInfo[] methodParams, int numberOfParameters)
        {
            var methodParameters = new Type[numberOfParameters];
            for (int j = 0; j < numberOfParameters; j++)
            {
                methodParameters[j] = methodParams[j].ParameterType;
            }
            return methodParameters;
        }

        private static MethodBuilder CreateMethodBuilder(MethodInfo methodInfo, Type[] methodParameters, TypeBuilder typeBuilder)
        {
            return typeBuilder.DefineMethod(methodInfo.Name,
                                            MethodAttributes.Public | MethodAttributes.Virtual,
                                            methodInfo.ReturnType,
                                            methodParameters);
        }
    }
}